/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package retrofit2;

import static retrofit2.Utils.throwIfFatal;

import java.io.IOException;
import java.util.Objects;

import androidx.annotation.Nullable;

import com.cxz.kotlin.baselibs.utils.AppManager;
import com.cxz.kotlin.baselibs.utils.LogUtil;
import com.cxz.kotlin.baselibs.utils.UserInfoUtil;
import com.fphoenixcorneae.util.toast.ToastUtil;

import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.ResponseBody;
import okhttp3.internal.connection.Exchange;
import okio.Buffer;
import okio.BufferedSource;
import okio.ForwardingSource;
import okio.Okio;
import okio.Timeout;

final class OkHttpCall<T> implements Call<T> {
    private final RequestFactory requestFactory;
    private final Object[] args;
    private final okhttp3.Call.Factory callFactory;
    private final Converter<ResponseBody, T> responseConverter;

    private volatile boolean canceled;

    private @Nullable
    okhttp3.Call rawCall;

    private @Nullable
    Throwable creationFailure;

    private boolean executed;

    OkHttpCall(
            RequestFactory requestFactory,
            Object[] args,
            okhttp3.Call.Factory callFactory,
            Converter<ResponseBody, T> responseConverter) {
        this.requestFactory = requestFactory;
        this.args = args;
        this.callFactory = callFactory;
        this.responseConverter = responseConverter;
    }

    @SuppressWarnings("CloneDoesntCallSuperClone")
    // We are a final type & this saves clearing state.
    @Override
    public OkHttpCall<T> clone() {
        return new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    }

    @Override
    public synchronized Request request() {
        try {
            return getRawCall().request();
        } catch (IOException e) {
            throw new RuntimeException("Unable to create request.", e);
        }
    }

    @Override
    public synchronized Timeout timeout() {
        try {
            return getRawCall().timeout();
        } catch (IOException e) {
            throw new RuntimeException("Unable to create call.", e);
        }
    }

    /**
     * Returns the raw call, initializing it if necessary. Throws if initializing the raw call throws,
     * or has thrown in previous attempts to create it.
     */
    private okhttp3.Call getRawCall() throws IOException {
        okhttp3.Call call = rawCall;
        if (call != null) return call;

        // Re-throw previous failures if this isn't the first attempt.
        if (creationFailure != null) {
            if (creationFailure instanceof IOException) {
                throw (IOException) creationFailure;
            } else if (creationFailure instanceof RuntimeException) {
                throw (RuntimeException) creationFailure;
            } else {
                throw (Error) creationFailure;
            }
        }

        // Create and remember either the success or the failure.
        try {
            return rawCall = createRawCall();
        } catch (RuntimeException | Error | IOException e) {
            throwIfFatal(e); // Do not assign a fatal error to creationFailure.
            creationFailure = e;
            throw e;
        }
    }

    @Override
    public void enqueue(final Callback<T> callback) {
        Objects.requireNonNull(callback, "callback == null");

        okhttp3.Call call;
        Throwable failure;

        synchronized (this) {
            if (executed) throw new IllegalStateException("Already executed.");
            executed = true;

            call = rawCall;
            failure = creationFailure;
            if (call == null && failure == null) {
                try {
                    call = rawCall = createRawCall();
                } catch (Throwable t) {
                    throwIfFatal(t);
                    failure = creationFailure = t;
                }
            }
        }

        if (failure != null) {
            callback.onFailure(this, failure);
            return;
        }

        if (canceled) {
            call.cancel();
        }

        call.enqueue(
                new okhttp3.Callback() {
                    @Override
                    public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
                        Response<T> response;

                        try {
                            if (rawResponse.code() == 221) {
                                ToastUtil.INSTANCE.show("登录失效");
                                UserInfoUtil.INSTANCE.logout(AppManager.getAppManager().currentActivity());
                                return;
                            }
                            response = parseResponse(rawResponse);

                        } catch (Throwable e) {

                            ResponseBody rawBody = rawResponse.body();
                            NoContentResponseBody noContentResponseBody = new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength());
                            LogUtil.d("noContentResponseBody->" + noContentResponseBody.toString());

                            throwIfFatal(e);
                            callFailure(e);
                            return;
                        }

                        try {
                            callback.onResponse(OkHttpCall.this, response);
                        } catch (Throwable t) {
                            throwIfFatal(t);
                            t.printStackTrace(); // TODO this is not great
                        }
                    }

                    @Override
                    public void onFailure(okhttp3.Call call, IOException e) {
                        callFailure(e);
                    }

                    private void callFailure(Throwable e) {
                        try {
                            callback.onFailure(OkHttpCall.this, e);
                        } catch (Throwable t) {
                            throwIfFatal(t);
                            t.printStackTrace(); // TODO this is not great
                        }
                    }
                });
    }

    @Override
    public synchronized boolean isExecuted() {
        return executed;
    }

    @Override
    public Response<T> execute() throws IOException {
        okhttp3.Call call;

        synchronized (this) {
            if (executed) throw new IllegalStateException("Already executed.");
            executed = true;

            call = getRawCall();
        }

        if (canceled) {
            call.cancel();
        }

        return parseResponse(call.execute());
    }

    private okhttp3.Call createRawCall() throws IOException {
        okhttp3.Call call = callFactory.newCall(requestFactory.create(args));
        if (call == null) {
            throw new NullPointerException("Call.Factory returned null.");
        }
        return call;
    }

    Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
        ResponseBody rawBody = rawResponse.body();

        // Remove the body's source (the only stateful object) so we can pass the response along.
        rawResponse =
                rawResponse
                        .newBuilder()
                        .body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
                        .build();

//    int code = rawResponse.code();
//    if (code < 200 || code >= 300) {
//      try {
//        // Buffer the entire body to avoid future I/O.
//        ResponseBody bufferedBody = Utils.buffer(rawBody);
//        return Response.error(bufferedBody, rawResponse);
//      } finally {
//        rawBody.close();
//      }
//    }
//
//    if (code == 204 || code == 205) {
//      rawBody.close();
//      return Response.success(null, rawResponse);
//    }

        ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody);
        try {
            T body = responseConverter.convert(catchingBody);
            return Response.success(body, rawResponse);
        } catch (RuntimeException e) {
            // If the underlying source threw an exception, propagate that rather than indicating it was
            // a runtime exception.
            catchingBody.throwIfCaught();
            throw e;
        }
    }

    @Override
    public void cancel() {
        canceled = true;

        okhttp3.Call call;
        synchronized (this) {
            call = rawCall;
        }
        if (call != null) {
            call.cancel();
        }
    }

    @Override
    public boolean isCanceled() {
        if (canceled) {
            return true;
        }
        synchronized (this) {
            return rawCall != null && rawCall.isCanceled();
        }
    }

    static final class NoContentResponseBody extends ResponseBody {
        private final @Nullable
        MediaType contentType;
        private final long contentLength;

        NoContentResponseBody(@Nullable MediaType contentType, long contentLength) {
            this.contentType = contentType;
            this.contentLength = contentLength;
        }

        @Override
        public MediaType contentType() {
            return contentType;
        }

        @Override
        public long contentLength() {
            return contentLength;
        }

        @Override
        public BufferedSource source() {
            throw new IllegalStateException("Cannot read raw response body of a converted body.");
        }
    }

    static final class ExceptionCatchingResponseBody extends ResponseBody {
        private final ResponseBody delegate;
        private final BufferedSource delegateSource;
        @Nullable
        IOException thrownException;

        ExceptionCatchingResponseBody(ResponseBody delegate) {
            this.delegate = delegate;
            this.delegateSource =
                    Okio.buffer(
                            new ForwardingSource(delegate.source()) {
                                @Override
                                public long read(Buffer sink, long byteCount) throws IOException {
                                    try {
                                        return super.read(sink, byteCount);
                                    } catch (IOException e) {
                                        thrownException = e;
                                        throw e;
                                    }
                                }
                            });
        }

        @Override
        public MediaType contentType() {
            return delegate.contentType();
        }

        @Override
        public long contentLength() {
            return delegate.contentLength();
        }

        @Override
        public BufferedSource source() {
            return delegateSource;
        }

        @Override
        public void close() {
            delegate.close();
        }

        void throwIfCaught() throws IOException {
            if (thrownException != null) {
                throw thrownException;
            }
        }
    }
}
